Algebraic Effects
2024-10
Effect System再考
実装がまちまちすぎるという話
結局のところ、「多様な制御構造を実現できる(がcall/ccやshift/resetより扱いやすい)」という話と、「多様な副作用をそれぞれ分離でき合成もしやすい」という話の2種類に分類できるのかな
1. 多様な副作用をトラッキングしたいという話
例えば、この関数を呼んでもIOが起こらないことを保証したいとか
Haskellはこれができる(型がIO xになるため)
例えばこの記事ではマーカーが付く(かつ伝播する)ことに重きが置かれていて実装を差し替えたいという話は特にない
2. エフェクト発生時の挙動をカスタマイズしたいという話
テスト時はダミーのDBにアクセスさせたいとか
これも継続を取れる/取れないというバリエーションがある
3. Effect Systemって限定継続に似てるじゃん→これを下回りとして例外やawaitを実装しよう という話
effectがあれば例外が実装できるのはわかりやすい
async/awaitが実装できる、というのはちょっとわかりにくいけどできるらしい
4. ハンドラを動的に差し替えたいという話題?
どのハンドラが使われるかは動的に決まる(極端な話、乱数で2つのうちのどちらかが選ばれるとか)
5. 高階のエフェクト
メモ
実行順序の入れ替え
バックトラック(こういうのcallccの説明例でよく出てくるよね)
Haskell・Scala界隈ではまた独自の発展が見られる気がするがどっちも詳しくない
こういうのが言語拡張でなくライブラリとして実装できるのがHaskellすごいなって思う
こんなにたくさんライブラリがあるってのはやっぱモナドの合成が辛いってのがモチベーションにあるんだろうか?
A B xではなく(A + B) x にしたい、みたいな
2022-06
R7RSのraise-continuableに似てるのかなぁ
Effect:(この文脈では)computational effect、いわゆる副作用全般
ヒープのread/write
I/O (コンソール、ネットワーク、データベース)
例外
Effect system:type→type systemみたいに、effect→effect system。effectの体系。型システムと一体化していることも。
Algebraic effects:プログラミング機構を指す。
call/ccやモナドみたいに、いろんな機能の下回りとして使えるものらしい。
call/ccよりは限定継続に近い
モナドと比べると、合成がかんたんなのがメリット
oneshot, multishotという種類がある
handlerがmultishotだと、ambが実装できたりする。その代わり処理系の実装は大変そう
async/awaitがEffectで書けるってどうやるんだろう
code:rb
def foo1
Http.get(url){|res|
p res
}
end
def foo2
var res = await Http.get(url)
p res
end
def foo3
handle("Http.get"){|url, k|
res = サーバにアクセスしてデータを取得
k(res)
}.run{
res = Http.get(url)
p res
}
end
こうか?
あとは、ファイルを削除するプログラムのテストを書きたいとか
code:rb
def foo
...
File.unlink(@log_path)
...
end
def foo
end
「ファイルを削除する」というeffectを定義する?
「移動する」「リネームする」とかすごくたくさんのeffectが要りそう
この場合、処理をしたあとは継続を単に呼び出すだけで、effectの強力な部分を使っているわけではない?
削除に時間がかかるケースを考えると、削除が終わるまでスケジューラに処理を戻して別の仕事をさせることができるかも
「同期的に削除する」「非同期的に削除する」「なにもしない(テスト用モック)」などの実装が考えられるが、いずれも利用側は同じコードでよい!
cf. 非同期版はawaitが要る、とか
effect handlerにどこまでを許すか
デバッグできなくなりそう
oneshotなら大丈夫かというと、処理順を逆にできたりする
「継続は最後に一度だけ呼ぶ」という制約を付けたら、どうなるだろうか
これだけでもいろいろ便利なことはあるはず
処理系の実装が簡単になったりしないかな
effect handlerはなぜ強いのか
エフェクト発生箇所以降の(部分)継続が暗黙的に取得できるところ
最少の道具で最大の機能を実現したい系の言語にはよさそう
#Shiika にeffect handlerを「取り入れる」意義はあるか(それが可能だとして) さまざまな制御構造の基礎として使えます→ちょっと強すぎ
副作用部分だけ差し替えることができます
→モックでもできる
同期で書いてあるものを非同期にするとかはさすがにできないけど
非同期(CPS)で書いてあれば、実装を同期にするのは可能
型を見れば副作用の種類と有無がわかる
これはちょっと、いいよね
しかも連鎖するので、間接的に呼ばれるものも含めて副作用がないことが保証される
Rustのunsafeみたいな
メモ
performはawaitと似ている
performは実装を差し替えられるが、awaitは差し替えられないというところが違う
(継続を最後に一度だけ呼び出す場合に限ると)performは「プログラムのこっからここまで」を穴にしている
それは普通のメソッド呼び出しも同じでは?
RubyやC#のモック:「このメソッドをこの引数で呼び出す」だけが固定されている
レシーバを差し替えることで挙動を変えられる
となるとやっぱ継続が渡されることが一番の違いなんだよな
既存実装はどう言っているか
「グローバル変数っぽいがグローバルではない」
依存関係の逆転とかDI
テスタビリティ
意図と実装の分離
これさあAffect.getsが何するのかわからんよね
追うのが大変そうじゃないかなって思った
型があれば戻り値の型くらいは分かるけど
現在時刻に応じてメッセージを表示する→「時刻の取得」と「メッセージを表示」が副作用
ベタ書きだとテストに困ったり、別の環境(eg. wasm)に移植するとき困る
従来ならinterface IOとかで差し替え可能にしていた場面
「dry-runが簡単にできる」
「副作用を分離する」ではあるけど、algebraic handlerぽくはないな
Promiseをthrowするやつって何だっけ?Suspenseか。
コンポーネントからPromiseをthrowすることで「まだレンダリングできない」を表現
また、そのPromiseを解決することで「レンダリングが可能になった」を表現
値を取得する関数がPromise<T>からただのT(+ throw)になるので、あたかも既にある値を取得するように見える(実際は無かったらReactが値が取得できるまで待って再実行するので、値があるときだけ残りの処理が行われる)
Suspenseだけだと、ボタン押す→データ受信待ち→結果描画となり、見た目が悪い
このためのuseTransition
また、細かいことをいえば、「fetchUsers()の結果が帰ってきたらsetStateする」という処理は命令的な書き方であり、宣言的にUIを記述する流れに逆行しています。
vs. "ロードできた部分から順次表示していくというパターン"
画面Bがデータを待つという部分も、Suspenseの機能およびFetcherによって、手続き的な部分がReactの内部に隠蔽され、宣言的な書き方ができています。
Suspenseをネストさせることで、データの到着順と表示状態を制御できる
逆から見れば、Reactに管理された世界は純粋な(副作用のない)世界です。例えば関数コンポーネントは副作用を発生させないことが期待されています。つまり、関数を呼び出しても(Reactの与り知らぬところで)何も起きないということです。この仮定があるからこそ、Reactは関数コンポーネントを好き勝手に呼び出すことができます。